import * as React from 'react';
import useScroll from './useScroll';
import getClientHeight from 'utils/getClientHeight';

type Position = {
  left: number;
  top: number;
};

type UseVirtualListOptions<T> = {
  /**
   * 原始数据，必须为数组
   */
  sourceData: T[];
  /**
   * 每一个item项的高度
   */
  itemHeight: number | ((item: T, index: number) => number);
  /**
   * 控制溢出可视区上 | 下的item项的数量，默认上下均为10
   */
  overscan?: number;
};
type StateActions<T> = [
  {
    /**
     * 外层可视区容器，只支持ref形式
     */
    viewportContainer: React.MutableRefObject<null>;
    /**
     * 内层列表容器，只支持ref形式
     */
    listContainer: React.MutableRefObject<null>;
    /**
     * 实际渲染数据
     */
    data: T[];
  },
  {
    /**
     * 当前位置
     * @member { left, top }
     */
    position: Position;
    /**
     * 手动获取滚动位置
     * @returns {Object} {left, top}
     */
    getPosition: () => Position;
    /**
     * 设置位置
     * @param {Object|undefined} position 传入当前dom需要到达的位置 { left: number. top: number }
     * @param {number|undefined} duration 到达指定位置的时间
     * @param {Function|undefined} callback 到达指定位置后的回调
     */
    setPosition: (
      position?: Partial<Position>,
      duration?: number,
      callback?: () => any
    ) => void;
  }
];

/**
 *
 * @param options 配置项 { sourceData, itemHeight, overscan }
 * @member sourceData 原始数据，必须为数组
 * @member itemHeight 每一个item项的高度
 * @member overscan 控制溢出可视区上 | 下的item项的数量，默认上下均为10
 * @returns [{ viewportContainer, listContainer, data },{ position, getPosition, setPosition }]
 * @member viewportContainer 外层可视区容器，只支持ref形式
 * @member listContainer 内层列表容器，只支持ref形式
 * @member data 实际渲染数据
 * @member position 当前容器所处的位置 { left, top }
 * @member getPosition 函数获取当前容器的位置
 * @member setPosition 设置当前容器的位置
 */
export default function useVirtualList<T>(
  options: UseVirtualListOptions<T>
): StateActions<T> {
  const { sourceData, itemHeight, overscan = 10 } = options || {};
  if (!Array.isArray(sourceData)) {
    throw new Error('sourceData must be an array!');
  }
  const [data, setData] = React.useState<T[]>([]);
  const [viewportContainer, { position, getPosition, setPosition }] =
    useScroll();

  const itemHeightRef = React.useRef(itemHeight);
  const listContainer = React.useRef(null);
  const viewportHeightRef = React.useRef(0);

  const itemHeightList = React.useMemo(() => {
    const itemHeight = itemHeightRef.current;
    return sourceData.map((item, index) =>
      typeof itemHeight === 'function' ? itemHeight(item, index) : itemHeight
    );
  }, [itemHeightRef, sourceData]);

  const scrollHeightToIns = React.useCallback(
    (top: number) => {
      const viewportHeight = viewportHeightRef.current;
      const heightToIns = {
        startIndex: 0,
        endIndex: 0,
        startSliceIndex: 0,
        endSliceIndex: 0,
        paddingTop: 0,
        paddingBottom: 0,
      };
      let topHeight = 0;
      itemHeightList.some((height, index) => {
        topHeight += height;
        if (topHeight >= top) {
          heightToIns.startIndex = index;
          return true;
        } else {
          return false;
        }
      });
      let listHeight = 0;
      itemHeightList.some((height, index) => {
        listHeight += height;
        if (listHeight >= top + viewportHeight) {
          heightToIns.endIndex = index;
          return true;
        } else {
          return false;
        }
      });
      const { startIndex, endIndex } = heightToIns;
      if (startIndex - overscan <= 0) {
        heightToIns.startSliceIndex = 0;
        heightToIns.endSliceIndex = endIndex + overscan + 1;
        heightToIns.paddingTop = 0;
        heightToIns.paddingBottom = itemHeightList
          .slice(heightToIns.endSliceIndex)
          .reduce((sum, height) => sum + height, 0);
      } else if (endIndex + overscan >= itemHeightList.length) {
        heightToIns.startSliceIndex = startIndex - overscan;
        heightToIns.endSliceIndex = itemHeightList.length;
        heightToIns.paddingTop = itemHeightList
          .slice(0, heightToIns.startSliceIndex)
          .reduce((sum, height) => sum + height, 0);
        heightToIns.paddingBottom = 0;
      } else {
        heightToIns.startSliceIndex = startIndex - overscan;
        heightToIns.endSliceIndex = endIndex + overscan + 1;
        heightToIns.paddingTop = itemHeightList
          .slice(0, heightToIns.startSliceIndex)
          .reduce((sum, height) => sum + height, 0);
        heightToIns.paddingBottom = itemHeightList
          .slice(heightToIns.endSliceIndex)
          .reduce((sum, height) => sum + height, 0);
      }
      return heightToIns;
    },
    [itemHeightList, overscan]
  );

  const handleScroll = React.useCallback(() => {
    const listTarget = listContainer.current;
    const { top } = getPosition();
    const { paddingTop, paddingBottom, startSliceIndex, endSliceIndex } =
      scrollHeightToIns(top);
    const newData = sourceData.slice(startSliceIndex, endSliceIndex);
    const newItemHeightList = itemHeightList.slice(
      startSliceIndex,
      endSliceIndex
    );
    if (listTarget) {
      (listTarget as HTMLElement).style.paddingTop = paddingTop + 'px';
      (listTarget as HTMLElement).style.paddingBottom = paddingBottom + 'px';
      window.setTimeout(() => {
        (listTarget as HTMLElement).childNodes.forEach((node, i) => {
          (node as HTMLElement).style.height = newItemHeightList[i] + 'px';
        });
      }, 0);
    }
    setData(newData);
  }, [getPosition, itemHeightList, sourceData, scrollHeightToIns]);

  React.useEffect(() => {
    const viewportTarget = viewportContainer.current;
    const listTarget = listContainer.current;
    let clientHeight = 0;
    if (viewportTarget) {
      clientHeight = getClientHeight(viewportTarget as HTMLElement);
    }
    if (viewportTarget) {
      (viewportTarget as HTMLElement).addEventListener('scroll', handleScroll);
    }
    viewportHeightRef.current = clientHeight;
    const { paddingTop, paddingBottom, startSliceIndex, endSliceIndex } =
      scrollHeightToIns(0);
    const newData = sourceData.slice(startSliceIndex, endSliceIndex);
    const newItemHeightList = itemHeightList.slice(
      startSliceIndex,
      endSliceIndex
    );
    if (listTarget) {
      (listTarget as HTMLElement).style.paddingTop = paddingTop + 'px';
      (listTarget as HTMLElement).style.paddingBottom = paddingBottom + 'px';
      window.setTimeout(() => {
        (listTarget as HTMLElement).childNodes.forEach((node, i) => {
          (node as HTMLElement).style.height = newItemHeightList[i] + 'px';
        });
      }, 0);
    }
    setData(newData);
    return () => {
      if (viewportTarget) {
        (viewportTarget as HTMLElement).removeEventListener(
          'scroll',
          handleScroll
        );
      }
    };
  }, [
    sourceData,
    itemHeightList,
    viewportContainer,
    handleScroll,
    scrollHeightToIns,
  ]);

  return [
    { viewportContainer, listContainer, data },
    { position, getPosition, setPosition },
  ];
}
